﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using OpenTK;
using OpenTK.Input;

using System.Runtime.InteropServices;

#if EMBEDDED
    using OpenTK.Graphics.ES20;
#else
using OpenTK.Graphics.OpenGL;
#endif

namespace MonoStrategy.RenderSystem
{

    [Flags]
    public enum TextureOptions
    {
        None = 0,
        Repeat = 1,
    }

    /// <summary>
    /// A native texture currently does nothing more than providing a convenient way
    /// to load bitmap images into the rendering pipeline. 
    /// </summary>
    public class NativeTexture : IDisposable
    {
        private Int32? m_ID = null;
        /// <summary>
        /// If a renderer is used, it will store a texture reference here for
        /// fast list operations.
        /// </summary>
        internal LinkedListNode<NativeTexture> Node { get; set; }
        /// <summary>
        /// The internal OpenGL texture ID.
        /// </summary>
        public Int32 ID { get { return m_ID.Value; } }

        public Int32 Width { get; private set; }
        public Int32 Height { get; private set; }
        public RectangleDouble ClipRect { get; private set; }
        public bool IsCompressed { get; private set; }

        /// <summary>
        /// Checks whether all unmanaged resources were released properly.
        /// </summary>
        ~NativeTexture()
        {
            // TODO:
            //if (m_ID.HasValue)
                //throw new ApplicationException("Texture has not been released before GC.");
        }

        /// <summary>
        /// Properly releases all unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (!m_ID.HasValue)
                return;

            int backup = ID;

            m_ID = null;

            GL.DeleteTexture(backup);

            GLRenderer.CheckError();            
        }


        /// <summary>
        /// If a renderer is given, the texture is registered for automatic 
        /// release. If you pass null, it's your duty to call <see cref="Dispose"/> before GC.
        /// </summary>
        public NativeTexture(Bitmap inImage)
            : this(TextureOptions.None, inImage)
        {
        }

        public NativeTexture(TextureOptions inOptions)
        {
            int[] tmpID = new int[1] { -1 };

            GL.GenTextures(1, tmpID);

            if (tmpID[0] == -1)
                throw new ApplicationException("Unable to allocate texture name.");

            GLRenderer.CheckError();

            m_ID = tmpID[0];

            Bind(0);

            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, (int)0);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, (int)0);

            if (inOptions.IsSet(TextureOptions.Repeat))
            {
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
            }
            else
            {
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
            }
        }

        public NativeTexture(TextureOptions inOptions, int inWidth, int inHeight, int[] inPixels)
            : this(inOptions)
        {
            Width = inWidth;
            Height = inHeight;

            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, inWidth, inHeight, 0, PixelFormat.Rgba, PixelType.UnsignedByte, inPixels);

            GLRenderer.CheckError();
        }


        public NativeTexture(TextureOptions inOptions, Bitmap inImage)
            : this(inOptions)
        {
            Width = inImage.Width;
            Height = inImage.Height;

            SetPixels(inImage);
        }

        public void SetPixels(Bitmap inImage)
        {
            System.Drawing.Imaging.BitmapData bmp_data = null;

            if ((inImage.Width != Width) || (inImage.Height != Height))
                throw new ArgumentException("Image dimensions do match.");

            try
            {
                bmp_data = inImage.LockBits(
                 new System.Drawing.Rectangle(0, 0, inImage.Width, inImage.Height),
                 System.Drawing.Imaging.ImageLockMode.ReadOnly,
                 System.Drawing.Imaging.PixelFormat.Format32bppArgb);

                Bind(0);

                GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, bmp_data.Width, bmp_data.Height, 0,
                    PixelFormat.Rgba, PixelType.UnsignedByte, bmp_data.Scan0);

                GLRenderer.CheckError();
            }
            finally
            {
                if (bmp_data != null)
                    inImage.UnlockBits(bmp_data);
            }
        }

        public bool IsCompressionSupported
        {
            get
            {
                int texFormatCount;
                int[] texFormatInts;

                GL.GetInteger(GetPName.NumCompressedTextureFormats, out texFormatCount); GLRenderer.CheckError();
                texFormatInts = new int[texFormatCount];
                GL.GetInteger(GetPName.CompressedTextureFormats, texFormatInts); GLRenderer.CheckError();

                /*
                 * We are using compression for animation library frames only, and therefore we need DXT3,
                 * since it is best suited for alpha masking...
                 */
                return texFormatInts.Contains((int)PixelInternalFormat.CompressedSrgbAlphaS3tcDxt3Ext);
            }
        }

        /// <summary>
        /// If image is already compressed, the following method will load it directly, omitting any unneccessary internal
        /// postprocessing in the OpenGL implementation/driver. But since compression is system dependent, you must also
        /// provide the original image in case the given compression is not supported. If this is the case, the method
        /// will behave exactly as <see cref="SetPixel"/>.
        /// </summary>
        public void SetPixels(Bitmap inImage, byte[] inCompressedPixels)
        {
            if (!IsCompressionSupported)
            {
                SetPixels(inImage);

                return;
            }

            Bind(0);

            GL.CompressedTexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.CompressedSrgbAlphaS3tcDxt3Ext, inImage.Width, inImage.Height, 0, inCompressedPixels.Length, inCompressedPixels);

            if (GL.GetError() != ErrorCode.NoError)
            {
                SetPixels(inImage);

                return;
            }
        }

        public void SetPixels(int inOffsetX, int inOffsetY, int inWidth, int inHeight, int[] inPixels)
        {
            int pixelCount = inWidth * inHeight;

            if ((inOffsetX + inWidth > Width) || (inOffsetY + inHeight > Height) || (pixelCount > inPixels.Length))
                throw new ArgumentOutOfRangeException();

            Bind(0);

            GL.TexSubImage2D(TextureTarget.Texture2D, 0, inOffsetX, inOffsetY, inWidth, inHeight, PixelFormat.Rgba, PixelType.UnsignedByte, inPixels); GLRenderer.CheckError();
        }

        /// <summary>
        /// Binds the texture to the rendering pipeline.
        /// </summary>
        public void Bind()
        {
            Bind(0);
        }

        public void Bind(int inStage)
        {
            //if (Renderer.CurrentTextureID == ID)
            //    return;

            switch (inStage)
            {
                case 0: GL.ActiveTexture(TextureUnit.Texture0); break;
                case 1: GL.ActiveTexture(TextureUnit.Texture1); break;
                case 2: GL.ActiveTexture(TextureUnit.Texture2); break;
                case 3: GL.ActiveTexture(TextureUnit.Texture3); break;
                case 4: GL.ActiveTexture(TextureUnit.Texture4); break;
                case 5: GL.ActiveTexture(TextureUnit.Texture5); break;
                case 6: GL.ActiveTexture(TextureUnit.Texture6); break;
                case 7: GL.ActiveTexture(TextureUnit.Texture7); break;
                default:
                    throw new ApplicationException();
            }

            GL.Enable(EnableCap.Texture2D);
            GL.BindTexture(TextureTarget.Texture2D, ID);
        }

        public void Unbind(int inStage)
        {
            switch (inStage)
            {
                case 0: GL.ActiveTexture(TextureUnit.Texture0); break;
                case 1: GL.ActiveTexture(TextureUnit.Texture1); break;
                case 2: GL.ActiveTexture(TextureUnit.Texture2); break;
                case 3: GL.ActiveTexture(TextureUnit.Texture3); break;
                case 4: GL.ActiveTexture(TextureUnit.Texture4); break;
                case 5: GL.ActiveTexture(TextureUnit.Texture5); break;
                case 6: GL.ActiveTexture(TextureUnit.Texture6); break;
                case 7: GL.ActiveTexture(TextureUnit.Texture7); break;
                default:
                    throw new ApplicationException();
            }

            GL.BindTexture(TextureTarget.Texture2D, 0);
            GL.Disable(EnableCap.Texture2D);
        }

        public void Save(String inFileName)
        {
            Bitmap image = new Bitmap(Width, Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            System.Drawing.Imaging.BitmapData bmp_data = null;

            using (image)
            {
                try
                {
                    bmp_data = image.LockBits(
                     new System.Drawing.Rectangle(0, 0, image.Width, image.Height),
                     System.Drawing.Imaging.ImageLockMode.ReadOnly,
                     System.Drawing.Imaging.PixelFormat.Format32bppArgb);

                    Bind(0);

                    GL.GetTexImage(TextureTarget.Texture2D, 0, PixelFormat.Rgba, PixelType.UnsignedByte, bmp_data.Scan0);

                    GLRenderer.CheckError();
                }
                finally
                {
                    if (bmp_data != null)
                        image.UnlockBits(bmp_data);
                }

                image.Save(inFileName);
            }
        }
    }
}
